package com.xdf.xzymanagementsystem.util;

import org.apache.ibatis.cache.Cache;
import org.apache.ibatis.cache.TransactionalCacheManager;
import org.apache.ibatis.cache.decorators.*;
import org.apache.ibatis.cache.impl.PerpetualCache;
import org.apache.ibatis.executor.CachingExecutor;
import org.apache.ibatis.io.Resources;
import org.apache.ibatis.session.SqlSession;
import org.apache.ibatis.session.SqlSessionFactory;
import org.apache.ibatis.session.SqlSessionFactoryBuilder;
import org.springframework.stereotype.Component;

import java.io.*;
import java.lang.reflect.Field;
import java.util.HashMap;
import java.util.Map;

/**
 * 2022年6月18日 08:42:41 chanchaw
 * 1. 私有化无参构造函数，要求初始化时必须传入配置文件路径。传入buildTransactionalCacheManager来构造本地属性：sqlSessionFactory，sqlSession，tcm
 * 2. 通过有参构造函数创建实例对象后即可使用 sqlSession 执行 mybatis 提供的增删改查方法
 * 3. 在执行过查询方法后需要调用 refresh 来构建实例内的缓存属性
 * 4. clearL2Cache 用于清理 mybatis 的二级缓存数据，本方法必须在调用 refresh 之后使用
 * 5. printL2Cache 可打印二级缓存的数据，切记最好调用 refresh 之后使用本方法，否则打印的可能是历史缓存
 */
@Component
public class MybatisUtils implements Serializable {

    private TransactionalCacheManager tcm;
    private SqlSessionFactory sqlSessionFactory;
    private SqlSession sqlSession;
    private Map<Cache, TransactionalCache> transactionalCacheMap = new HashMap<Cache, TransactionalCache>();
    private Map<Object,Object> l2Cache = new HashMap<Object, Object>();
    private String configFile;

    private static final long serialVersionUID = 1L;

    public void test1(SqlSessionFactory sqlSessionFactory) throws NoSuchFieldException, IllegalAccessException, IOException {
        this.sqlSessionFactory = sqlSessionFactory;
        this.sqlSession = sqlSessionFactory.openSession();

        // 获取 TransactionalCacheManager
        Field executorField = this.sqlSession.getClass().getDeclaredField("executor");
        executorField.setAccessible(true);
        CachingExecutor cachingExecutor = (CachingExecutor) executorField.get(sqlSession);
        Field tcmField = cachingExecutor.getClass().getDeclaredField("tcm");
        tcmField.setAccessible(true);
        this.tcm = (TransactionalCacheManager)tcmField.get(cachingExecutor);

        refresh();
    }

    // 打印二级缓存对象
    public void printL2Cache(){
        for (Map.Entry<Object, Object> entry : this.l2Cache.entrySet()) {
            byte[] bytes = (byte[]) entry.getValue();
            System.out.println("key:" + entry.getKey() + ",value:" + bytes2Object(bytes));
        }
    }

    public void clearL2Cache() throws IllegalAccessException, NoSuchFieldException, IOException {
        if(this.tcm == null)
            throw new RuntimeException("没有实例化本类对象或者 TransactionalCacheManager 对象初始化失败！");
        if(this.transactionalCacheMap == null || this.transactionalCacheMap.size()<=0)
            throw new RuntimeException("请在调用refresh方法之后调用本方法");

        for (Map.Entry<Cache, TransactionalCache> entry : this.transactionalCacheMap.entrySet()) {
            this.tcm.clear(entry.getKey());
        }
        // 清除本类的二级缓存数据 - 同步为 tcm 内的一样的状态
        this.l2Cache.clear();
    }

    // 刷新后获取缓存对象
    // 每要获取最新缓存对象时都要通过反射获取一次，如果拿本类中的属性，则是历史数据
    public void refresh() throws NoSuchFieldException, IllegalAccessException, IOException {
        if(this.configFile == null || this.configFile.length()<=0) throw new RuntimeException("没有传入配置文件路径！");
//        getTransactionalCacheManager(this.configFile);
        if(this.tcm == null) throw new RuntimeException("没有初始化 TransactionalCacheManager!");

        // 获取 L2 Cache Map 对象
        this.transactionalCacheMap = buildTransactionalCacheMap(this.tcm);
        this.l2Cache.clear();// 由于本方法会重新从 tcm.transactionalCacheMap 中读取缓存数据，要先清理该属性

        // 根据 transactionalCacheMap 获取实际的二级缓存对象 Map
        for (Map.Entry<Cache, TransactionalCache> entry : transactionalCacheMap.entrySet()) {
            TransactionalCache transactionalCacheItem = entry.getValue();
            //Cache delegate
            Field delegateField = transactionalCacheItem.getClass().getDeclaredField("delegate");
            delegateField.setAccessible(true);
            SynchronizedCache synchronizedCacheDelegate = (SynchronizedCache)delegateField.get(transactionalCacheItem);

            // Cache delegate
            Field loggingCacheDelegateField = synchronizedCacheDelegate.getClass().getDeclaredField("delegate");
            loggingCacheDelegateField.setAccessible(true);
            LoggingCache loggingCacheDelegate = (LoggingCache)loggingCacheDelegateField.get(synchronizedCacheDelegate);

            //Cache delegate
            Field serializedCacheDelegateField = loggingCacheDelegate.getClass().getDeclaredField("delegate");
            serializedCacheDelegateField.setAccessible(true);
            SerializedCache serializedCacheDelegate = (SerializedCache)serializedCacheDelegateField.get(loggingCacheDelegate);

            //Cache delegate
            Field lruCacheDelegateField = serializedCacheDelegate.getClass().getDeclaredField("delegate");
            lruCacheDelegateField.setAccessible(true);
            LruCache lruCacheDelegate = (LruCache)lruCacheDelegateField.get(serializedCacheDelegate);

            //Cache delegate
            Field perpetualCacheDelegateField = lruCacheDelegate.getClass().getDeclaredField("delegate");
            perpetualCacheDelegateField.setAccessible(true);
            PerpetualCache perpetualCacheDelegate = (PerpetualCache)perpetualCacheDelegateField.get(lruCacheDelegate);

            //Map<Object, Object> cache
            Field cacheField = perpetualCacheDelegate.getClass().getDeclaredField("cache");
            cacheField.setAccessible(true);
            Map<Object, Object> tmpCache = (Map<Object, Object>)cacheField.get(perpetualCacheDelegate);
            this.l2Cache.putAll(tmpCache);
        }
    }

    private MybatisUtils(){}
    public MybatisUtils(String configFile) throws IOException, NoSuchFieldException, IllegalAccessException {
        this.configFile = configFile;
        //buildTransactionalCacheManager(configFile);
    }

    private void buildTransactionalCacheManager(String configFile) throws IOException, NoSuchFieldException, IllegalAccessException {
        this.configFile = configFile;
        InputStream inputStream = Resources.getResourceAsStream(configFile);
        this.sqlSessionFactory = new SqlSessionFactoryBuilder().build(inputStream);
        this.sqlSession = sqlSessionFactory.openSession();

        // 获取 TransactionalCacheManager
        Field executorField = sqlSession.getClass().getDeclaredField("executor");
        executorField.setAccessible(true);
        CachingExecutor cachingExecutor = (CachingExecutor) executorField.get(sqlSession);
        Field tcmField = cachingExecutor.getClass().getDeclaredField("tcm");
        tcmField.setAccessible(true);
        this.tcm = (TransactionalCacheManager)tcmField.get(cachingExecutor);
        // 可以将截止到获取 tcm 的逻辑放入构造函数中
        // 下面的逻辑必须在执行一次查询后才能获取到对象实例
    }

    // 返回 L2 Cache 的缓存对象
    private Map<Cache, TransactionalCache> buildTransactionalCacheMap(TransactionalCacheManager tcm) throws NoSuchFieldException, IllegalAccessException {
        Field transactionalCachesField = tcm.getClass().getDeclaredField("transactionalCaches");
        transactionalCachesField.setAccessible(true);
        Map<Cache, TransactionalCache> transactionalCaches = (Map<Cache, TransactionalCache>)transactionalCachesField.get(tcm);
        return transactionalCaches;
    }

    // 将字节数组转换为 Object 类型对象
    private Object bytes2Object(byte[] bytes){
        try{
            ByteArrayInputStream byteArrayInputStream = new ByteArrayInputStream(bytes);
            ObjectInputStream objectInputStream = new ObjectInputStream(byteArrayInputStream);
            return objectInputStream.readObject();
        } catch(IOException e){
            return null;
        } catch(ClassNotFoundException e){
            return null;
        }
    }

    // 将对象转换为字节数组
    private byte[] object2Bytes(Object o) throws IOException {
        try{
            ByteArrayOutputStream byteArrayOutputStream = new ByteArrayOutputStream();
            ObjectOutputStream objectOutputStream = new ObjectOutputStream(byteArrayOutputStream);
            objectOutputStream.writeObject(o);
        } catch (IOException e){
            return null;
        }
        return null;
    }

    public TransactionalCacheManager getTcm() {
        return tcm;
    }

    public void setTcm(TransactionalCacheManager tcm) {
        this.tcm = tcm;
    }

    public SqlSessionFactory getSqlSessionFactory() {
        return sqlSessionFactory;
    }

    public void setSqlSessionFactory(SqlSessionFactory sqlSessionFactory) {
        this.sqlSessionFactory = sqlSessionFactory;
    }

    public SqlSession getSqlSession() {
        return sqlSession;
    }

    public void setSqlSession(SqlSession sqlSession) {
        this.sqlSession = sqlSession;
    }

    public Map<Cache, TransactionalCache> getTransactionalCacheMap() {
        return transactionalCacheMap;
    }

    public void setTransactionalCacheMap(Map<Cache, TransactionalCache> transactionalCacheMap) {
        this.transactionalCacheMap = transactionalCacheMap;
    }

    public Map<Object, Object> getL2Cache() {
        return l2Cache;
    }

    public void setL2Cache(Map<Object, Object> l2Cache) {
        this.l2Cache = l2Cache;
    }

    public String getConfigFile() {
        return configFile;
    }

    public void setConfigFile(String configFile) {
        this.configFile = configFile;
    }

    @Override
    public String toString() {
        return "MybatisUtils{" +
                "tcm=" + tcm +
                ", sqlSessionFactory=" + sqlSessionFactory +
                ", sqlSession=" + sqlSession +
                ", transactionalCacheMap=" + transactionalCacheMap +
                ", l2Cache=" + l2Cache +
                ", configFile='" + configFile + '\'' +
                '}';
    }
}